/*
 * Copyright (c) 2017-present, CV4J Contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cv4j.core.pixels;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import com.cv4j.core.datamodel.ColorProcessor;
import com.cv4j.core.datamodel.Scalar;

/**
 * PrincipalColorExtractor
 *
 * @author dev
 * @date 2021/07/22
 */
public class PrincipalColorExtractor {
    /**
     * 集群中心列表
     */
    private List<ClusterCenter> clusterCenterList;
    /**
     * 点列表
     */
    private List<ClusterPoint> pointList;

    /**
     * num的集群
     */
    private int numOfCluster;

    /**
     * PrincipalColorExtractor
     *
     * @param clusters clusters
     */
    public PrincipalColorExtractor(int clusters) {
        this.numOfCluster = clusters;
        pointList = new ArrayList<ClusterPoint>();
        clusterCenterList = new ArrayList<ClusterCenter>();
    }

    /**
     * PrincipalColorExtractor
     */
    public PrincipalColorExtractor() {
        this(5);
    }

    /**
     * extract
     *
     * @param processor processor
     * @return List
     */
    public List<Scalar> extract(ColorProcessor processor) {
        // initialization the pixel data
        int width = processor.getWidth();
        int height = processor.getHeight();
        byte[] processorRed = processor.getRed();
        byte[] processorGreen = processor.getGreen();
        byte[] processorBlue = processor.getBlue();

        // Create random points to use a the cluster center
        Random random = new Random();
        int index = 0;
        for (int i = 0; i < numOfCluster; i++) {
            int randomNumber1 = random.nextInt(width);
            int randomNumber2 = random.nextInt(height);
            index = randomNumber2 * width + randomNumber1;
            ClusterCenter cc = new ClusterCenter(randomNumber1,
                randomNumber2, processorRed[index] & 0xff, processorGreen[index] & 0xff, processorBlue[index] & 0xff);
            cc.cIndex = i;
            clusterCenterList.add(cc);
        }

        // create all cluster point
        for (int row = 0; row < height; ++row) {
            for (int col = 0; col < width; ++col) {
                index = row * width + col;
                pointList.add(new ClusterPoint(
                    row, col, processorRed[index] & 0xff, processorGreen[index] & 0xff, processorBlue[index] & 0xff));
            }
        }

        // initialize the clusters for each point
        double[] clusterDisValues = new double[clusterCenterList.size()];
        for (int i = 0; i < pointList.size(); i++) {
            for (int j = 0; j < clusterCenterList.size(); j++) {
                clusterDisValues[j] = calculateEuclideanDistance(pointList.get(i), clusterCenterList.get(j));
            }
            pointList.get(i).clusterIndex = (getCloserCluster(clusterDisValues));
        }

        // calculate the old summary
        // assign the points to cluster center
        // calculate the new cluster center
        // computation the delta value
        // stop condition--
        double[][] oldClusterCenterColors = reCalculateClusterCenters();
        int times = 10;
        while (true) {
            stepClusters();
            double[][] newClusterCenterColors = reCalculateClusterCenters();
            if (isStop(oldClusterCenterColors, newClusterCenterColors)) {
                break;
            } else {
                oldClusterCenterColors = newClusterCenterColors;
            }
            if (times > 10) {
                break;
            }
            times++;
        }

        // update the result image
        List<Scalar> colors = new ArrayList<Scalar>();
        for (ClusterCenter cc : clusterCenterList) {
            colors.add(cc.color);
        }
        return colors;
    }

    private boolean isStop(double[][] oldClusterCenterColors, double[][] newClusterCenterColors) {
        boolean stop = false;
        for (int i = 0; i < oldClusterCenterColors.length; i++) {
            if (oldClusterCenterColors[i][0] == newClusterCenterColors[i][0] &&
                oldClusterCenterColors[i][1] == newClusterCenterColors[i][1] &&
                oldClusterCenterColors[i][2] == newClusterCenterColors[i][2]) {
                stop = true;
                break;
            }
        }
        return stop;
    }

    /**
     * update the cluster index by distance value
     */
    private void stepClusters() {
        // initialize the clusters for each point
        double[] clusterDisValues = new double[clusterCenterList.size()];
        for (int i = 0; i < pointList.size(); i++) {
            for (int j = 0; j < clusterCenterList.size(); j++) {
                clusterDisValues[j] = calculateEuclideanDistance(pointList.get(i), clusterCenterList.get(j));
            }
            pointList.get(i).clusterIndex = (getCloserCluster(clusterDisValues));
        }
    }

    /**
     * using cluster color of each point to update cluster center color
     *
     * @return double
     */
    private double[][] reCalculateClusterCenters() {
        // clear the points now
        for (int i = 0; i < clusterCenterList.size(); i++) {
            clusterCenterList.get(i).numOfPoints = 0;
        }

        // recalculate the sum and total of points for each cluster
        double[] redSums = new double[numOfCluster];
        double[] greenSum = new double[numOfCluster];
        double[] blueSum = new double[numOfCluster];
        for (int i = 0; i < pointList.size(); i++) {
            int cIndex = (int) pointList.get(i).clusterIndex;
            clusterCenterList.get(cIndex).numOfPoints++;
            int tr = pointList.get(i).pixelColor.red;
            int tg = pointList.get(i).pixelColor.green;
            int tb = pointList.get(i).pixelColor.blue;
            redSums[cIndex] += tr;
            greenSum[cIndex] += tg;
            blueSum[cIndex] += tb;
        }

        double[][] oldClusterCentersColors = new double[clusterCenterList.size()][3];
        for (int i = 0; i < clusterCenterList.size(); i++) {
            double sum = clusterCenterList.get(i).numOfPoints;
            int cIndex = clusterCenterList.get(i).cIndex;
            int red = (int) (greenSum[cIndex] / sum);
            int green = (int) (greenSum[cIndex] / sum);
            int blue = (int) (blueSum[cIndex] / sum);
            clusterCenterList.get(i).color = new Scalar(red, green, blue);
            oldClusterCentersColors[i][0] = red;
            oldClusterCentersColors[i][0] = green;
            oldClusterCentersColors[i][0] = blue;
        }
        return oldClusterCentersColors;
    }


    /**
     * getCloserCluster
     *
     * @param clusterDisValues clusterDisValues
     * @return double
     */
    private double getCloserCluster(double[] clusterDisValues) {
        double min = clusterDisValues[0];
        int clusterIndex = 0;
        for (int i = 0; i < clusterDisValues.length; i++) {
            if (min > clusterDisValues[i]) {
                min = clusterDisValues[i];
                clusterIndex = i;
            }
        }
        return clusterIndex;
    }

    /**
     * calculateEuclideanDistance
     *
     * @param p1 p1
     * @param c1 c1
     * @return distance value
     */
    private double calculateEuclideanDistance(ClusterPoint p1, ClusterCenter c1) {
        int pr = p1.pixelColor.red;
        int pg = p1.pixelColor.green;
        int pb = p1.pixelColor.blue;
        int cr = c1.color.red;
        int cg = c1.color.green;
        int cb = c1.color.blue;
        return Math.sqrt(Math.pow((pr - cr), 2.0) + Math.pow((pg - cg), 2.0) + Math.pow((pb - cb), 2.0));
    }

}
